#include "driverjack.h"
#include <QStringList>
#include <QWaitCondition>
#include <QMutex>
#include "globals.h"
#include "aiprocessor.h"

using namespace DJ;

struct DriverJack::AudioPortStruct {
    DJ::Sample * buffer;
    bool isOutput;
    jack_port_t * jackPort;
    QStringList connectedPortsNames;
};

struct DriverJack::StaticData {
    static int 		process(jack_nframes_t nframes, void *unused_arg);
    static int 		srate (jack_nframes_t nframes, void *unused_arg);
    static void 	error (const char *desc);
    static void 	jack_shutdown (void *unused_arg);
    static int 		bufsize_callback (jack_nframes_t  nframes, void *unused_arg);

    jack_nframes_t 	sampleRate;
    jack_nframes_t 	bufferSize;
    jack_client_t * 	client;
    AudioPortStruct audioPortsList [DJ::MAX_N_PORTS];

    QSharedPointer<AIProcessor> processor;

    int NPortsRegistered;
    QWaitCondition buffersReady;
    bool isFinishing;
};

DriverJack::StaticData DriverJack::staticData_;

// ====================  STATIC SECTION (CALLBACKS) BEGIN
//
int DriverJack::StaticData::process (jack_nframes_t nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    DJ::Sample * tmpBuffer;
    AudioPortStruct * audioPort;
    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        audioPort = staticData_.audioPortsList + i;
        if (audioPort->buffer != NULL &&
                audioPort->jackPort != NULL) {
            tmpBuffer = (DJ::Sample *) jack_port_get_buffer (audioPort->jackPort, nFrames);
            if (audioPort->isOutput) {
                memcpy(tmpBuffer, audioPort->buffer, sizeof(DJ::Sample)*nFrames);
            } else {
                memcpy(audioPort->buffer, tmpBuffer, sizeof(DJ::Sample)*nFrames);
            }
        }
    }
    staticData_.buffersReady.wakeAll();
    return 0;
}

int DriverJack::StaticData::srate (jack_nframes_t nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    fprintf (stderr, "JD: now the sample rate is %lu/sec\n", (long unsigned int)nFrames);
    staticData_.sampleRate = nFrames;
    return 0;
}

void DriverJack::StaticData::error (const char *desc)
{
    fprintf (stderr, "JD: JACK error: %s\n", desc);
}

void DriverJack::StaticData::jack_shutdown (void *unused_arg)
{
    Q_UNUSED(unused_arg);
    //   fprintf (stderr, "JD: JACK server is shuting down! \n");
    //exit (1);
}

int DriverJack::StaticData::bufsize_callback (jack_nframes_t  nFrames, void *unused_arg)
{
    Q_UNUSED(unused_arg);
    fprintf (stderr, "JD: buffer size is now %lu/sec\n", (long unsigned int)nFrames);
    staticData_.bufferSize = nFrames;
    //staticData_.setBufferSize(nFrames);
    return 0;
}
//
// ====================  STATIC SECTION (CALLBACKS) END
//
DriverJack::DriverJack(QSharedPointer<AIProcessor> processor, const char * nameOfClient) :
    driverState_(CREATED),
    nameOfClient_(nameOfClient),
    nameOfDriver_ (DJ::NAME_OF_DRIVER_JACK)
{
    staticData_.NPortsRegistered = 0;
    staticData_.bufferSize = 0;
    staticData_.client = NULL;
    staticData_.processor = processor;
    staticData_.isFinishing = true;
    gl::log("JD: ...construction ok ");
}

void DriverJack::startAll(){

    init();
    AIProcessor::PData d(new gl::AudioPorts(2, 2, staticData_.bufferSize));

    registerPort( (DJ::Sample*) d->outputs[0], true, "OUT" + QString("L"));
    registerPort((DJ::Sample*)  d->outputs[1] , true, "OUT" +  QString("R"));

    registerPort((DJ::Sample*) d->inputs[0], false, "IN" +  QString("L"));
    registerPort((DJ::Sample*) d->inputs[1], false, "IN" +  QString("R"));

    if (startJack()!=0) return;
    connectDefaultPorts();
    staticData_.isFinishing = false;
    staticData_.processor->setData(d);
    this->start(); // start processing thread
}

void DriverJack::stopAll(){
    staticData_.isFinishing = true;
    wait(1);
    stopJack();
    clearPorts();
}


DJ::ReturnCode DriverJack::init()
{
    if (driverState_ != CREATED) {
        gl::log(QString( "JD: cannot init driver - not created state"));
        return -1;
    }
    gl::log("JD: initialization started...");

    if (nameOfClient_.size() == 0) {
        gl::log(QString("JD: jack client name is not defined"));
        return -1;
    }

    jack_set_error_function (StaticData::error);
    // open client, register callbacks, actvate client
    if ((staticData_.client = jack_client_open (nameOfClient_.toAscii().data(), JackNullOption, NULL)) == 0) {
        gl::log(QString("JD: jack server not running?\n"));
        return -1;
    }
    jack_set_process_callback (staticData_.client, StaticData::process, 0);
    gl::log(QString("..set_process_callback"));
    jack_set_sample_rate_callback (staticData_.client, StaticData::srate, 0);
    gl::log(QString("..set_sample_rate_callback"));

    // doesnt work on WIN should be reconsidered
    //    jack_on_shutdown (staticData_.client, StaticData::jack_shutdown, 0);
    //    gl::log(QString("..on_shutdown"));

    jack_set_buffer_size_callback (staticData_.client, StaticData::bufsize_callback, 0);
    gl::log(QString("..buffer_size_callback"));

    staticData_.sampleRate = jack_get_sample_rate (staticData_.client);
    staticData_.bufferSize = jack_get_buffer_size (staticData_.client);

    gl::log(QString("JD: sample rate : %1, buffer size : %2 ").arg(staticData_.sampleRate).arg(staticData_.bufferSize));

    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        staticData_.audioPortsList[i].jackPort = NULL;
        staticData_.audioPortsList[i].buffer = NULL;
    }
    driverState_ = INITED;
    return 0;
}

DriverJack::~DriverJack()
{
    gl::log("JD: exiting...");
    stopAll();
    if (driverState_ != STOPED )
        if (stopJack() != 0) {gl::log("JD: proper deconstruction failed - cant stop");
        };

    gl::log("JD: cleanup...");


    AudioPortStruct * ap; // saving connections of client
    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        ap = & (staticData_.audioPortsList[i]);
        if (! ap->connectedPortsNames.isEmpty()) ap->connectedPortsNames.clear();
    }

    gl::log("JD: ...closing client ");

    if (staticData_.client != NULL) jack_client_close (staticData_.client);
    //usleep(100000);
    gl::log("JD: ...deconstruction ok ");
}

DJ::ReturnCode DriverJack::startJack()
{
    if (! (driverState_ == STOPED || driverState_ == INITED)) {
        gl::log(QString( "JD: cannot start driver - not stoped or inited"));
        return -1;//DJ::JACK_CANT_ACTIVATE;
    }
    gl::log("JD: starting jack client...");

    if (staticData_.client == NULL ) {
        gl::log(QString( "JD: cannot activate client"));
        return -1;//DJ::JACK_CANT_ACTIVATE;
    }

    if (jack_activate (staticData_.client) == 0) {
        gl::log(QString( "JD: client is activated and started"));
        driverState_ = STARTED;
        return 0;//DJ::SUCCESS;
    } else {
        gl::log(QString( "JD: client cant be activated, unknown error")) ;
        return -1;//DJ::JACK_CANT_ACTIVATE;
    }
}

DJ::ReturnCode DriverJack::stopJack()
{
    if (driverState_ != STARTED ) {
        gl::log(QString( "JD: cannot stop driver, it has been not started yet, but trying to deactivate anyway"));
        return -1; // DJ::JACK_CANT_ACTIVATE;
    }
    gl::log("JD: stopping jack client...");
    if (staticData_.client == NULL /*|| ! staticData_.isClientActive*/ ) {
        gl::log(QString( "JD: nothing to deactivate - no client (NULL pointer)"));
        return -1;//DJ::JACK_CANT_DEACTIVATE;
    }

    if (jack_deactivate (staticData_.client) == 0) {
        //staticData_.isClientActive = false;
        gl::log(QString( "JD: client is deactivated"));
        driverState_ = STOPED;
        return 0; //DJ::SUCCESS;
    }
    gl::log(QString( "JD: client can not be deactivated"));
    return -1;//DJ::JACK_CANT_DEACTIVATE;
}

void DriverJack::run(){  // QThread running function
    gl::log("ENG: thread started...");
    QMutex mutex;
    while (not staticData_.isFinishing){
        mutex.lock();
        this->staticData_.buffersReady.wait(&mutex);
        staticData_.processor->process();
        mutex.unlock();
    }
    gl::log("ENG: thread stopped...");
}

const QString & DriverJack::getNameOfDriver()
{
    return nameOfDriver_;
}

QStringList * DriverJack::getPhysicalPortNames(bool isJackOutput, bool isAudio)
{
    unsigned long jpf =  JackPortIsPhysical |
            (isJackOutput ? JackPortIsOutput : JackPortIsInput);
    const char ** physicalPorts;
    if ((physicalPorts = jack_get_ports (staticData_.client, NULL, NULL, jpf)) == NULL) {
        gl::log("JD: can not find any physical ports");
        return NULL;
    }
    QStringList * ports = new QStringList();

    for (int i = 0; physicalPorts[i] != NULL; i++) {
        if (QString((isAudio ? JACK_DEFAULT_AUDIO_TYPE : JACK_DEFAULT_MIDI_TYPE)) ==
                QString(jack_port_type(jack_port_by_name (staticData_.client, physicalPorts[i])))
                ) {
            ports->append(QString(physicalPorts[i]));
            gl::log("JD: physical port found: " + QString(physicalPorts[i]));
        }
    }
    jack_free(physicalPorts);
    return ports;
}


int DriverJack::getBufferSize() {return staticData_.bufferSize;}
int DriverJack::getSampleRate() {return staticData_.sampleRate;}

int DriverJack::registerPort(DJ::Sample * buffer, bool isOutput, QString name)
{
    gl::log("JD: registering port : <"+ name + ">...");
    if (staticData_.NPortsRegistered == DJ::MAX_N_PORTS ||
            staticData_.client == NULL ||
            buffer == NULL) {
        gl::log("JD: can not register port");
        return -1;
    }
    int portID = -1;
    for (int i = 0; i < DJ::MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort == NULL &&
                staticData_.audioPortsList[i].buffer == NULL) {
            portID = i;
            break;
        }
    }
    if (portID == -1 ) {
        gl::log("JD: number of ports exeeded maximum");
        return -1; // free record not found
    }
    if (name.isEmpty()) name =
            QString(isOutput ? AUDIO_OUT_PORT_PREFIX : AUDIO_IN_PORT_PREFIX).arg(
                portID, N_DIGITS_IN_A_PORT_NAME, 10, QChar('0'));

    jack_port_t * jackPort = jack_port_register (staticData_.client,
                                                 name.toLatin1().data(),
                                                 JACK_DEFAULT_AUDIO_TYPE,
                                                 (isOutput ? JackPortIsOutput : JackPortIsInput),
                                                 0);

    if (jackPort == NULL) {
        gl::log("JD: port registration was rejected by jack server");
        return -1;
    }

    staticData_.NPortsRegistered++;
    staticData_.audioPortsList[portID].buffer = buffer;
    staticData_.audioPortsList[portID].isOutput = isOutput;
    staticData_.audioPortsList[portID].jackPort = jackPort;
    gl::log("JD: port is registered : " + name);
    return portID;
}

QString DriverJack::setPortName(int portID, const QString & portName)
{
    if (staticData_.audioPortsList[portID].jackPort == NULL ||
            portName.isEmpty()) {
        gl::log("JD: cant change portname to : " + portName);
        return "";
    }

    jack_port_set_name	(	staticData_.audioPortsList[portID].jackPort,
                                portName.toLatin1().data());
    gl::log("JD: new port name is : " +  QString(jack_port_short_name(staticData_.audioPortsList[portID].jackPort)));
    return jack_port_short_name(staticData_.audioPortsList[portID].jackPort);
}

DJ::ReturnCode DriverJack::deletePort(const int portID)
{
    stopJack();
    if (jack_port_unregister (
                staticData_.client, staticData_.audioPortsList[portID].jackPort))	{
        delete [] staticData_.audioPortsList[portID].buffer;
        staticData_.audioPortsList[portID].buffer = NULL;
        staticData_.audioPortsList[portID].jackPort = NULL;
        startJack();
        return -1; //DJ::SUCCESS;
    }
    startJack();
    return -1; //DJ::JACK_CANT_UNREGISTER_PORT;
}

DJ::ReturnCode DriverJack::clearPorts ()
{

    stopJack();
    for (int portID = 0; portID<DJ::MAX_N_PORTS; ++portID){
        if (staticData_.audioPortsList[portID].jackPort != NULL)
            if (jack_port_unregister (
                        staticData_.client, staticData_.audioPortsList[portID].jackPort))	{
                delete [] staticData_.audioPortsList[portID].buffer;
                staticData_.audioPortsList[portID].buffer = NULL;
                staticData_.audioPortsList[portID].jackPort = NULL;
            }
        startJack();
        staticData_.NPortsRegistered = 0;

    }
    return 0; //DJ::SUCCESS;
}


QStringList * DriverJack::getMyPortNames()
{
    QStringList * myPortNames = new QStringList();
    for (int i = 0; i < MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort != NULL) {
            myPortNames->append(
                        QString(jack_port_short_name(staticData_.audioPortsList[i].jackPort)));
        }
    }
    return myPortNames;
}

DJ::ReturnCode
DriverJack::connectPort( const int myPortID, const QString & extPortName ){
    if (driverState_ != STARTED ) { //DJ::JACK_CANT_CONNECT_PORT;
        gl::log(QString(
                    "JD: can't connect to port: " +
                    extPortName + ", start driver first!"));
        return -1; }
    QString source;
    QString destination;
    if (staticData_.audioPortsList[myPortID].isOutput) {
        source = jack_port_name(staticData_.audioPortsList[myPortID].jackPort);
        destination = extPortName;
    } else {
        destination = jack_port_name(staticData_.audioPortsList[myPortID].jackPort);
        source = extPortName;
    }

    if (jack_connect (staticData_.client,source.toAscii().data(), destination.toAscii().data()) == 0)
    {
        gl::log("JD: ports connected: " + source + " to: " + destination);
        return 0; //DJ::SUCCESS;
    } else {
        gl::log("JD: can't connect " + source + " to: " + destination);
        return -1;//DJ::JACK_CANT_CONNECT_PORT;
    }
}

DJ::ReturnCode	DriverJack::connectDefaultPorts()
{
    // connecting first two inputs of the host to two physical outputs of the card
    // and vice versa, to provide basic operation mode
    gl::log("JD: connecting defaults..");
    int nOutputs = 0;
    int nInputs = 0;
    QStringList * cardOuts = getPhysicalPortNames(false, true);// card's out = jackd's in
    QStringList * cardIns = getPhysicalPortNames(true, true);
    for (int i = 0; i < MAX_N_PORTS; i++) {
        if (staticData_.audioPortsList[i].jackPort != NULL) {
            jack_port_disconnect(staticData_.client, staticData_.audioPortsList[i].jackPort);
            if (staticData_.audioPortsList[i].isOutput) {
                if (nOutputs < 2 && (cardOuts->size() - nOutputs) > 0) {
                    connectPort(i, (*cardOuts)[nOutputs]);
                    nOutputs++;
                }
            }	else {
                if (nInputs < 2 && (cardIns->size() - nInputs) > 0) {
                    connectPort(i, (*cardIns)[nInputs]);
                    nInputs++;
                }
            }
        }
    }
    gl::log("JD: connecting defaults done.");
    delete cardIns;
    delete cardOuts;
    return 0;//DJ::SUCCESS;
}
